﻿//***************************************************************************
// Contributed by Ondrej Dvorak, 2013: Changes with respect to the DSLPed 
// project
//
// Copyright © 2010 Microsoft Corporation.  All Rights Reserved.
// This code released under the terms of the 
// Microsoft Public License (MS-PL, http://opensource.org/licenses/ms-pl.html.)
//***************************************************************************
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Xml.Linq;
using Dsl.Ped.Designer.Controllers;
using Dsl.Ped.Designer.Utils;
using Dsl.Ped.Designer.Views;
using Dsl.Ped.Editor;
using Dsl.Ped.Editor.Interfaces;
using Dsl.Ped.Framework.AddIns;
using Dsl.Ped.Framework.Messages;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Package;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.XmlEditor;

namespace Dsl.Ped.Designer.ViewModels
{
    /// <summary>
    /// ViewModel of the <see cref="DslPedDesignerControl"/>
    /// </summary>
    public class DslPedDesignerControlViewModel : IDslPedDesignerControlViewModel
    {
        #region Fields

        /// <summary>
        /// Unified service provider for managing the VS package corresponding
        /// with this extension
        /// </summary>
        private readonly IServiceProvider serviceProvider;

        /// <summary>
        /// Instance of <see cref="DSLPedEditor"/> which is encapsulated by this extension
        /// </summary>
        private readonly IDslPedEditor dslPedEditor;

        /// <summary>
        /// Name of the file that was opened in this editor
        /// </summary>
        private readonly string fileName;

        /// <summary>
        /// MEF composition container all available DSLPed add-ins are loaded by this 
        /// composition container
        /// </summary>
        private CompositionContainer compositionContainer;

        private XmlModel xmlModel;
        private XmlStore xmlStore;

        private IVsTextLines buffer;

        private bool synchronizing;

        private long dirtyTime;

        private EventHandler<XmlEditingScopeEventArgs> editingScopeCompletedHandler;
        private EventHandler<XmlEditingScopeEventArgs> undoRedoCompletedHandler;
        private EventHandler bufferReloadedHandler;

        LanguageService xmlLanguageService;

        bool? canEditFile;
        bool gettingCheckoutStatus;

        /// <summary>
        /// Gets or sets all AddIns available in Visual Studio and implementing the <see cref="IDslPedAddIn"/>
        /// These are injected to the method via MEF when the corresponding composition container is executed
        /// </summary>
        [ImportMany(typeof(IDslPedAddIn))]
        private IEnumerable<Lazy<IDslPedAddIn, IDslPedAddInMetadata>> availableAddIns { get; set; }

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="DslPedDesignerControlViewModel"/> class. 
        /// </summary>
        /// <param name="xmlStore">XML storage</param>
        /// <param name="xmlModel">XML model</param>
        /// <param name="serviceProvider">Service provider</param>
        /// <param name="buffer">Text buffer</param>
        /// <param name="fileName">The file Name.</param>
        public DslPedDesignerControlViewModel(XmlStore xmlStore, XmlModel xmlModel, IServiceProvider serviceProvider, IVsTextLines buffer, string fileName)
        {
            // xmlModel must not be empty
            if (xmlModel == null)
            {
                throw new ArgumentNullException("xmlModel");
            }

            // xmlStore must not be empty
            if (xmlStore == null)
            {
                throw new ArgumentNullException("xmlStore");
            }

            this.BufferDirty = false;
            this.DesignerDirty = false;
            this.UndoRedoCompleted = false;

            this.serviceProvider = serviceProvider;
            this.buffer = buffer;

            this.xmlStore = xmlStore;
            this.xmlModel = xmlModel;

            this.fileName = fileName;

            // OnUnderlyingEditCompleted
            this.editingScopeCompletedHandler = this.OnUnderlyingEditCompleted;
            this.xmlStore.EditingScopeCompleted += this.editingScopeCompletedHandler;

            // OnUndoRedoCompleted
            this.undoRedoCompletedHandler = this.OnUndoRedoCompleted;
            this.xmlStore.UndoRedoCompleted += this.undoRedoCompletedHandler;
            
            this.bufferReloadedHandler += this.OnBufferReloaded;
            this.xmlModel.BufferReloaded += this.bufferReloadedHandler;

            // Initialize AddIns that are available in the editor
            this.InitializeAddIns();

            // Create new instance of a DSLPed editor based on the available add-ins
            this.dslPedEditor = new DslPedEditor(this.availableAddIns);

            this.LoadModelFromXmlModel();
        }

        #endregion
        
        #region Members
        
        /// <summary>
        /// Property indicating if there is a pending change in the underlying text buffer
        /// that needs to be reflected in the ViewModel.
        /// 
        /// Used by DoIdle to determine if we need to sync.
        /// </summary>
        public bool BufferDirty { get; set; }

        public bool UndoRedoCompleted { get; set; }

        /// <summary>
        /// Property indicating if there is a pending change in the ViewModel that needs to 
        /// be committed to the underlying text buffer.
        /// 
        /// Used by DoIdle to determine if we need to sync.
        /// </summary>
        public bool DesignerDirty
        {
            get
            {
                return this.dslPedEditor.IsDirty;
            }
            set
            {
                if (this.dslPedEditor != null)
                {
                    this.dslPedEditor.IsDirty = value;
                }
            }
        }

        /// <summary>
        /// We must not try and update the XDocument while the XML DslPedEditor2 is parsing as this may cause
        /// a deadlock in the XML DslPedEditor2!
        /// </summary>
        /// <returns></returns>
        bool IsXmlEditorParsing
        {
            get
            {
                LanguageService langsvc = this.GetXmlLanguageService();
                return langsvc != null ? langsvc.IsParsing : false;
            }
        }

        public IDslPedEditor DSLPedEditor
        {
            get
            {
                return this.dslPedEditor;
            }
        }

        #endregion

        #region Events

        public event EventHandler ViewModelChanged;

        #endregion

        #region Methods

        /// <summary>
        /// Called on idle time. This is when we check if the designer is out of sync with the underlying text buffer.
        /// </summary>
        public void DoIdle()
        {
            if (this.BufferDirty || this.DesignerDirty)
            {
                const int delay = 100;

                if ((Environment.TickCount - this.dirtyTime) > delay)
                {
                    // Must not try and sync while XML editor is parsing otherwise we just confuse matters.
                    if (this.IsXmlEditorParsing)
                    {
                        this.dirtyTime = Environment.TickCount;
                        return;
                    }

                    // If there is contention, give the preference to the designer.
                    if (this.DesignerDirty)
                    {
                        this.SaveModelToXmlModel();
                        
                        // We don't do any merging, so just overwrite whatever was in the buffer.
                        this.BufferDirty = false;
                    }
                    //else if (BufferDirty)
                    //{
                    //    // TODO - sync from xml to editor doesn't work, if it is commented
                    //    //LoadModelFromXmlModel();
                    //} 
                    else if (this.UndoRedoCompleted)
                    {
                        this.LoadModelFromXmlModel();
                        this.UndoRedoCompleted = false;
                    }
                }
            }
        }

        /// <summary>
        /// Handle undo/redo completion event.  This happens when the user invokes Undo/Redo on a buffer edit operation.
        /// We need to resync when this happens.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnUndoRedoCompleted(object sender, XmlEditingScopeEventArgs e)
        {
            if (!this.synchronizing)
            {
                this.BufferDirty = true;
                this.UndoRedoCompleted = true;
                this.dirtyTime = Environment.TickCount;
            }
        }

        /// <summary>
        /// Handle edit scope completion event. This happens when the XML editor buffer decides to update
        /// it's XDocument parse tree.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnUnderlyingEditCompleted(object sender, XmlEditingScopeEventArgs e)
        {
            if (e.EditingScope.UserState != this && !this.synchronizing)
            {
                this.BufferDirty = true;
                this.dirtyTime = Environment.TickCount;
            }
        }

        private void OnBufferReloaded(object sender, EventArgs e)
        {
            if (!this.synchronizing)
            {
                this.BufferDirty = true;
                this.dirtyTime = Environment.TickCount;
            }
        }

        /// <summary>
        /// Get the XML DslPedEditor <see cref="LanguageService"/>
        /// </summary>
        /// <returns></returns>
        private LanguageService GetXmlLanguageService()
        {
            if (this.xmlLanguageService == null)
            {
                var vssp = this.serviceProvider.GetService(typeof(Microsoft.VisualStudio.OLE.Interop.IServiceProvider)) as Microsoft.VisualStudio.OLE.Interop.IServiceProvider;
                var xmlEditorGuid = new Guid("f6819a78-a205-47b5-be1c-675b3c7f0b8e");
                var iunknown = new Guid("00000000-0000-0000-C000-000000000046");
                IntPtr ptr;
                if (ErrorHandler.Succeeded(vssp.QueryService(ref xmlEditorGuid, ref iunknown, out ptr)))
                {
                    try
                    {
                        this.xmlLanguageService = Marshal.GetObjectForIUnknown(ptr) as LanguageService;
                    }
                    finally
                    {
                        Marshal.Release(ptr);
                    }
                }
            }
            return this.xmlLanguageService;
        }

        /// <summary>
        /// This method is called when it is time to save the designer values to the
        /// underlying buffer.
        /// </summary>
        private void SaveModelToXmlModel()
        {
            LanguageService langsvc = this.GetXmlLanguageService();

            try
            {
                //We can't edit this file (perhaps the user cancelled a SCC prompt, etc...)
                if (!this.CanEditFile())
                {
                    this.DesignerDirty = false;
                    this.BufferDirty = true;
                    throw new Exception();
                }

                var documentFromDesignerState = this.dslPedEditor.SerializeProjectiveObject();

                // This is a kind of lock, to avoid collision while the XML model is extracted
                // out of the model
                this.synchronizing = true;
                XDocument document = this.GetParseTree();
                Source src = this.GetSource();
                if (src == null || langsvc == null)
                {
                    return;
                }

                langsvc.IsParsing = true; // lock out the background parse thread.

                // Wrap the buffer sync and the formatting in one undo unit.
                using (CompoundAction ca = new CompoundAction(src, Resources.SynchronizeBuffer))
                {
                    using (XmlEditingScope scope = this.xmlStore.BeginEditingScope(Resources.SynchronizeBuffer, this))
                    {
                        //Replace the existing XDocument with the new one we just generated.
                        document.Root.ReplaceWith(documentFromDesignerState.Root);
                        scope.Complete();
                    }
                    ca.FlushEditActions();
                    this.FormatBuffer(src);
                }
                this.DesignerDirty = false;
            }
            catch (Exception)
            {
                // if the synchronization fails then we'll just try again in a second.
                this.dirtyTime = Environment.TickCount;
            }
            finally
            {
                langsvc.IsParsing = false;

                // Release the lock
                this.synchronizing = false;
            }
        }

        /// <summary>
        /// This function asks the QueryEditQuerySave service if it is possible to edit the file.
        /// This can result in an automatic checkout of the file and may even prompt the user for
        /// permission to checkout the file.  If the user says no or the file cannot be edited 
        /// this returns false.
        /// </summary>
        private bool CanEditFile()
        {
            // Cache the value so we don't keep asking the user over and over.
            if (this.canEditFile.HasValue)
            {
                return (bool)this.canEditFile;
            }

            // Check the status of the recursion guard
            if (this.gettingCheckoutStatus)
                return false;

            this.canEditFile = false; // assume the worst
            try
            {
                // Set the recursion guard
                this.gettingCheckoutStatus = true;

                // Get the QueryEditQuerySave service
                IVsQueryEditQuerySave2 queryEditQuerySave = this.serviceProvider.GetService(typeof(SVsQueryEditQuerySave)) as IVsQueryEditQuerySave2;

                string filename = this.xmlModel.Name;

                // Now call the QueryEdit method to find the edit status of this file
                string[] documents = { filename };
                uint result;
                uint outFlags;

                // Note that this function can popup a dialog to ask the user to checkout the file.
                // When this dialog is visible, it is possible to receive other request to change
                // the file and this is the reason for the recursion guard
                int hr = queryEditQuerySave.QueryEditFiles(
                    0,              // Flags
                    1,              // Number of elements in the array
                    documents,      // Files to edit
                    null,           // Input flags
                    null,           // Input array of VSQEQS_FILE_ATTRIBUTE_DATA
                    out result,     // result of the checkout
                    out outFlags);    // Additional flags

                if (ErrorHandler.Succeeded(hr) && (result == (uint)tagVSQueryEditResult.QER_EditOK))
                {
                    // In this case (and only in this case) we can return true from this function
                    this.canEditFile = true;
                }
            }
            finally
            {
                this.gettingCheckoutStatus = false;
            }
            return (bool)this.canEditFile;
        }

        /// <summary>
        /// Load the model from the underlying text buffer.
        /// </summary>
        private void LoadModelFromXmlModel()
        {
            using (var reader = this.GetParseTree().CreateReader())
            {
                var result = this.dslPedEditor.LoadFile(this.fileName, reader);

                // Monitoring of Error Messages
                var vsController = new VSController(this.serviceProvider);

                FrameworkMessenger.RegisterParent(result);
                vsController.Start(FrameworkMessenger.GetMessenger(result));

                this.dslPedEditor.OpenLoadedFile();
            }

            this.BufferDirty = false;

            if (this.ViewModelChanged != null)
            {
                // Update the Designer View
                this.ViewModelChanged(this, new EventArgs());
            }
        }

        /// <summary>
        /// Get an <see cref="XDocument"/> parse tree that is up-to-date
        /// </summary>
        /// <returns><see cref="XDocument"/> parse tree</returns>
        private XDocument GetParseTree()
        {
            LanguageService langsvc = this.GetXmlLanguageService();

            // don't crash if the language service is not available
            if (langsvc != null)
            {
                Source src = langsvc.GetSource(this.buffer);

                // We need to access this method to get the most up to date parse tree.
                // public virtual XmlDocument GetParseTree(Source source, IVsTextView view, int line, int col, ParseReason reason) {
                MethodInfo mi = langsvc.GetType().GetMethod("GetParseTree");
                int line = 0, col = 0;
                mi.Invoke(langsvc, new object[] { src, null, line, col, ParseReason.Check });
            }

            // Now the XmlDocument should be up to date also.
            return this.xmlModel.Document;
        }

        /// <summary>
        /// Initialize all add-ins that are available to the editor
        /// </summary>
        private void InitializeAddIns()
        {
            var catalog = new AggregateCatalog();
            var cm = VsUtils.GetComponentModel();
            this.compositionContainer = new CompositionContainer(catalog);
            catalog.Catalogs.Add(cm.DefaultCatalog);

            try
            {
                this.compositionContainer.ComposeParts(this);
            }
            catch (Exception exc)
            {
                Debug.WriteLine("Error: " + exc);
            }
        }

        /// <summary>
        /// Get the XML DslPedEditor Source object for this document.
        /// </summary>
        /// <returns></returns>
        private Source GetSource()
        {
            LanguageService langsvc = this.GetXmlLanguageService();
            if (langsvc == null)
            {
                return null;
            }
            Source src = langsvc.GetSource(this.buffer);
            return src;
        }

        /// <summary>
        /// Reformat the text buffer
        /// </summary>
        private void FormatBuffer(Source src)
        {
            using (EditArray edits = new EditArray(src, null, false, Resources.ReformatBuffer))
            {
                TextSpan span = src.GetDocumentSpan();
                src.ReformatSpan(edits, span);
            }
        }

        #endregion
    }
}
